/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.hilo.boot.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;

import org.springframework.http.HttpInputMessage;

/**
 * Delegating implementation of {@link javax.servlet.ServletInputStream}.
 *
 * <p>Used by {@link MockHttpServletRequest}; typically not directly
 * used for testing application controllers.
 *
 * @author Juergen Hoeller
 * @since 1.0.2
 * @see MockHttpServletRequest
 */
public class DelegatingServletInputStream extends ServletInputStream {

    private InputStream sourceStream;

    private boolean finished = false;

    public DelegatingServletInputStream() {
    }
    
    /**
     * Create a DelegatingServletInputStream for the given source stream.
     */
    public DelegatingServletInputStream(InputStream sourceStream) throws IOException {
        setSourceStream(sourceStream);
    }

    @Override
    public int read() throws IOException {
        int data = this.sourceStream.read();
        if (data == -1) {
            this.finished = true;
        }
        return data;
    }

    @Override
    public int available() throws IOException {
        return this.sourceStream != null ? this.sourceStream.available() : 0;
    }

    @Override
    public void close() throws IOException {
        if (this.sourceStream != null) {
            this.sourceStream.close();
        }
    }

    @Override
    public boolean isFinished() {
        try {
            if (sourceStream.markSupported()) {
                sourceStream.mark(1);
                if (sourceStream.read() == -1) {
                    return true;
                }
                sourceStream.reset();
                return false;
            } else if (sourceStream instanceof PushbackInputStream) {
                PushbackInputStream pushbackInputStream = (PushbackInputStream) sourceStream;
                int b = pushbackInputStream.read();
                if (b == -1) {
                    return true;
                } else {
                    pushbackInputStream.unread(b);
                    return false;
                }
            }
        } catch (IOException e) {
            // ignore
            return true;
        }
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
    }

    /**
     * Return the underlying source stream (never {@code null}).
     */
    public final InputStream getSourceStream() {
        return this.sourceStream;
    }

    public void setSourceStream(InputStream sourceStream) throws IOException {
        this.sourceStream = wrapInputStream(sourceStream);
    }
    
    /**
     * java.io.PushbackInputStream原理：
     * 
     * read()是输出，一开始是从原始数据源（inputStream）读取。
     * unread(int b)是输入（相当于write），将int b输入到（byte[] buf）缓存中。
     * 
     * 当buf缓存中有数据之后，read()就会优先从buf中读取。
     * 
     * buf默认是一个length为1的数组，当有unread（即write）被调用时，
     * 会将数据保存在buf中（从后往前添加）。
     * 
     * 当buf中有数据之后，read()会优先从buf中读取（从前往后）。
     * 
     * 举个例子：
     * 
     * 1、read取出2字节 = [10,11]
     * 2、unread取出的2字节。buf= [10,11]
     * 3、read取出2字节 = [10,11] （这次是从buf中取出）
     * 
     * 性能分析：
     *  read的时候，仅仅多了一步判断是否有buf数据，性能并无明显降低。
     */
    protected InputStream wrapInputStream(InputStream inputStream) throws IOException {
        InputStream body;
        if (inputStream.markSupported()) {
            inputStream.mark(1);
            body = (inputStream.read() != -1 ? inputStream : null);
            inputStream.reset();
        }
        else {
            PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
            int b = pushbackInputStream.read();
            if (b == -1) {
                body = null;
            }
            else {
                body = pushbackInputStream;
                pushbackInputStream.unread(b);
            }
        }
        return body;
    }

}
